#include <gio/gio.h>
#include <glib-object.h>
#include <clutter/clutter.h>

/* {{{ MenuItemModel */

/* This is our "model" of a Menu item; it has a "label" property, and
 * a "selected" state property. The user is supposed to operate on the
 * model instance, and change its state.
 */

#define EXAMPLE_TYPE_MENU_ITEM_MODEL (example_menu_item_model_get_type ())

G_DECLARE_FINAL_TYPE (ExampleMenuItemModel,
                      example_menu_item_model,
                      EXAMPLE,
                      MENU_ITEM_MODEL,
                      GObject)

struct _ExampleMenuItemModel
{
    GObject parent_instance;

    char* label;

    gboolean selected;
};

struct _ExampleMenuItemModelClass
{
    GObjectClass parent_class;
};

enum
{
    MENU_ITEM_MODEL_PROP_LABEL = 1,
    MENU_ITEM_MODEL_PROP_SELECTED,
    MENU_ITEM_MODEL_N_PROPS
};

static GParamSpec* menu_item_model_props[MENU_ITEM_MODEL_N_PROPS] = {
    NULL,
};

G_DEFINE_TYPE (ExampleMenuItemModel, example_menu_item_model, G_TYPE_OBJECT)

static void
example_menu_item_model_finalize (GObject* gobject)
{
    ExampleMenuItemModel* self = (ExampleMenuItemModel*)gobject;

    g_free (self->label);

    G_OBJECT_CLASS (example_menu_item_model_parent_class)->finalize (gobject);
}

static void
example_menu_item_model_set_property (GObject* gobject,
                                      guint prop_id,
                                      const GValue* value,
                                      GParamSpec* pspec)
{
    ExampleMenuItemModel* self = (ExampleMenuItemModel*)gobject;

    switch (prop_id)
    {
        case MENU_ITEM_MODEL_PROP_LABEL:
            g_free (self->label);
            self->label = g_value_dup_string (value);
            break;

        case MENU_ITEM_MODEL_PROP_SELECTED:
            self->selected = g_value_get_boolean (value);
            break;
    }
}

static void
example_menu_item_model_get_property (GObject* gobject,
                                      guint prop_id,
                                      GValue* value,
                                      GParamSpec* pspec)
{
    ExampleMenuItemModel* self = (ExampleMenuItemModel*)gobject;

    switch (prop_id)
    {
        case MENU_ITEM_MODEL_PROP_LABEL:
            g_value_set_string (value, self->label);
            break;

        case MENU_ITEM_MODEL_PROP_SELECTED:
            g_value_set_boolean (value, self->selected);
            break;
    }
}

static void
example_menu_item_model_class_init (ExampleMenuItemModelClass* klass)
{
    GObjectClass* gobject_class = G_OBJECT_CLASS (klass);

    gobject_class->set_property = example_menu_item_model_set_property;
    gobject_class->get_property = example_menu_item_model_get_property;
    gobject_class->finalize = example_menu_item_model_finalize;

    menu_item_model_props[MENU_ITEM_MODEL_PROP_LABEL] = g_param_spec_string (
        "label", NULL, NULL, NULL, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
    menu_item_model_props[MENU_ITEM_MODEL_PROP_SELECTED] =
        g_param_spec_boolean ("selected",
                              NULL,
                              NULL,
                              FALSE,
                              G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
    g_object_class_install_properties (
        gobject_class, MENU_ITEM_MODEL_N_PROPS, menu_item_model_props);
}

static void
example_menu_item_model_init (ExampleMenuItemModel* self)
{
}
/* }}} */

/* {{{ MenuItemView */

/* This is our "view" of a Menu item; it changes state depending on whether
 * the "selected" property is set. The "view" reflects the state of the
 * "model" instance, though it has no direct connection to it.
 */
#define EXAMPLE_TYPE_MENU_ITEM_VIEW (example_menu_item_view_get_type ())

G_DECLARE_FINAL_TYPE (ExampleMenuItemView,
                      example_menu_item_view,
                      EXAMPLE,
                      MENU_ITEM_VIEW,
                      ClutterText)

struct _ExampleMenuItemView
{
    ClutterText parent_instance;

    gboolean is_selected;
};

struct _ExampleMenuItemViewClass
{
    ClutterTextClass parent_class;
};

G_DEFINE_TYPE (ExampleMenuItemView, example_menu_item_view, CLUTTER_TYPE_TEXT)

enum
{
    MENU_ITEM_VIEW_PROP_SELECTED = 1,
    MENU_ITEM_VIEW_N_PROPS
};

static GParamSpec* menu_item_view_props[MENU_ITEM_VIEW_N_PROPS] = {
    NULL,
};

static void
example_menu_item_view_set_selected (ExampleMenuItemView* self,
                                     gboolean selected)
{
    selected = !!selected;
    if (self->is_selected == selected)
        return;

    self->is_selected = selected;

    if (self->is_selected)
        clutter_text_set_color (CLUTTER_TEXT (self),
                                CLUTTER_COLOR_LightSkyBlue);
    else
        clutter_text_set_color (CLUTTER_TEXT (self), CLUTTER_COLOR_White);

    g_object_notify_by_pspec (
        G_OBJECT (self), menu_item_view_props[MENU_ITEM_VIEW_PROP_SELECTED]);
}

static void
example_menu_item_view_set_property (GObject* gobject,
                                     guint prop_id,
                                     const GValue* value,
                                     GParamSpec* pspec)
{
    switch (prop_id)
    {
        case MENU_ITEM_VIEW_PROP_SELECTED:
            example_menu_item_view_set_selected (
                EXAMPLE_MENU_ITEM_VIEW (gobject), g_value_get_boolean (value));
            break;
    }
}

static void
example_menu_item_view_get_property (GObject* gobject,
                                     guint prop_id,
                                     GValue* value,
                                     GParamSpec* pspec)
{
    switch (prop_id)
    {
        case MENU_ITEM_VIEW_PROP_SELECTED:
            g_value_set_boolean (value,
                                 EXAMPLE_MENU_ITEM_VIEW (gobject)->is_selected);
            break;
    }
}

static void
example_menu_item_view_class_init (ExampleMenuItemViewClass* klass)
{
    GObjectClass* gobject_class = G_OBJECT_CLASS (klass);

    gobject_class->set_property = example_menu_item_view_set_property;
    gobject_class->get_property = example_menu_item_view_get_property;

    menu_item_view_props[MENU_ITEM_VIEW_PROP_SELECTED] =
        g_param_spec_boolean ("selected",
                              NULL,
                              NULL,
                              FALSE,
                              G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

    g_object_class_install_properties (
        gobject_class, MENU_ITEM_VIEW_N_PROPS, menu_item_view_props);
}

static void
example_menu_item_view__transition_stopped (ClutterActor* actor,
                                            const char* transition,
                                            gboolean is_finished)
{
    clutter_actor_set_scale (actor, 1.0, 1.0);
    clutter_actor_set_opacity (actor, 255);
}

static void
example_menu_item_view_init (ExampleMenuItemView* self)
{
    ClutterText* text = CLUTTER_TEXT (self);
    ClutterActor* actor = CLUTTER_ACTOR (self);
    ClutterTransition *scalex_trans, *scaley_trans, *fade_trans;
    ClutterTransition* group;

    clutter_text_set_font_name (text, "Sans Bold 24px");
    clutter_text_set_color (text, CLUTTER_COLOR_White);

    clutter_actor_set_margin_left (actor, 12);
    clutter_actor_set_margin_right (actor, 12);

    clutter_actor_set_pivot_point (actor, 0.5, 0.5);

    scalex_trans = clutter_property_transition_new ("scale-x");
    clutter_transition_set_from (scalex_trans, G_TYPE_FLOAT, 1.0);
    clutter_transition_set_to (scalex_trans, G_TYPE_FLOAT, 3.0);

    scaley_trans = clutter_property_transition_new ("scale-y");
    clutter_transition_set_from (scaley_trans, G_TYPE_FLOAT, 1.0);
    clutter_transition_set_to (scaley_trans, G_TYPE_FLOAT, 3.0);

    fade_trans = clutter_property_transition_new ("opacity");
    clutter_transition_set_to (fade_trans, G_TYPE_UINT, 0);

    group = clutter_transition_group_new ();
    clutter_transition_group_add_transition (CLUTTER_TRANSITION_GROUP (group),
                                             scalex_trans);
    clutter_transition_group_add_transition (CLUTTER_TRANSITION_GROUP (group),
                                             scaley_trans);
    clutter_transition_group_add_transition (CLUTTER_TRANSITION_GROUP (group),
                                             fade_trans);
    clutter_timeline_set_duration (CLUTTER_TIMELINE (group), 250);
    clutter_timeline_set_progress_mode (CLUTTER_TIMELINE (group),
                                        CLUTTER_EASE_OUT);

    clutter_actor_add_transition (actor, "activateTransition", group);
    g_object_unref (group);

    clutter_timeline_stop (CLUTTER_TIMELINE (group));

    g_signal_connect (actor,
                      "transition-stopped",
                      G_CALLBACK (example_menu_item_view__transition_stopped),
                      group);
}

static void
example_menu_item_view_activate (ExampleMenuItemView* self)
{
    ClutterTransition* t;

    t = clutter_actor_get_transition (CLUTTER_ACTOR (self),
                                      "activateTransition");
    clutter_timeline_start (CLUTTER_TIMELINE (t));
}

/* }}} */

/* {{{ Menu */

/* This is our container actor, which binds the GListStore with the
 * ExampleMenuItemModel instances to the ExampleMenuItemView actors
 */

#define EXAMPLE_TYPE_MENU (example_menu_get_type ())

G_DECLARE_FINAL_TYPE (ExampleMenu, example_menu, EXAMPLE, MENU, ClutterActor)

struct _ExampleMenu
{
    ClutterActor parent_instance;

    int current_idx;
};

struct _ExampleMenuClass
{
    ClutterActorClass parent_class;
};

G_DEFINE_TYPE (ExampleMenu, example_menu, CLUTTER_TYPE_ACTOR)

static void
example_menu_class_init (ExampleMenuClass* klass)
{
}

static void
example_menu_init (ExampleMenu* self)
{
    ClutterActor* actor = CLUTTER_ACTOR (self);
    ClutterLayoutManager* layout;

    layout = clutter_box_layout_new ();
    clutter_box_layout_set_orientation (CLUTTER_BOX_LAYOUT (layout),
                                        CLUTTER_ORIENTATION_VERTICAL);
    clutter_box_layout_set_spacing (CLUTTER_BOX_LAYOUT (layout), 12);

    clutter_actor_set_layout_manager (actor, layout);
    clutter_actor_set_background_color (actor, CLUTTER_COLOR_Black);

    self->current_idx = -1;
}

static ClutterActor*
example_menu_select_item (ExampleMenu* self, int idx)
{
    ClutterActor* item;

    /* Any change in the view is reflected into the model */

    if (idx == self->current_idx)
        return clutter_actor_get_child_at_index (CLUTTER_ACTOR (self),
                                                 self->current_idx);

    item = clutter_actor_get_child_at_index (CLUTTER_ACTOR (self),
                                             self->current_idx);
    if (item != NULL)
        example_menu_item_view_set_selected ((ExampleMenuItemView*)item, FALSE);

    if (idx < 0)
        idx = clutter_actor_get_n_children (CLUTTER_ACTOR (self)) - 1;
    else if (idx >= clutter_actor_get_n_children (CLUTTER_ACTOR (self)))
        idx = 0;

    self->current_idx = idx;

    item = clutter_actor_get_child_at_index (CLUTTER_ACTOR (self),
                                             self->current_idx);
    if (item != NULL)
        example_menu_item_view_set_selected ((ExampleMenuItemView*)item, TRUE);

    return item;
}

static ClutterActor*
example_menu_select_next (ExampleMenu* self)
{
    return example_menu_select_item (self, self->current_idx + 1);
}

static ClutterActor*
example_menu_select_prev (ExampleMenu* self)
{
    return example_menu_select_item (self, self->current_idx - 1);
}

static void
example_menu_activate_item (ExampleMenu* self)
{
    ClutterActor* child;

    child = clutter_actor_get_child_at_index (CLUTTER_ACTOR (self),
                                              self->current_idx);
    if (child == NULL)
        return;

    example_menu_item_view_activate ((ExampleMenuItemView*)child);
}

/* }}} */

/* {{{ main */
static gboolean
on_key_press (ClutterActor* stage, ClutterEvent* event)
{
    ClutterActor* scroll = clutter_actor_get_first_child (stage);
    ClutterActor* menu = clutter_actor_get_first_child (scroll);
    ClutterActor* item = NULL;
    guint key = clutter_event_get_key_symbol (event);
    ClutterPoint p;

    switch (key)
    {
        case CLUTTER_KEY_q:
            clutter_main_quit ();
            break;

        case CLUTTER_KEY_Up:
            item = example_menu_select_prev ((ExampleMenu*)menu);
            clutter_actor_get_position (item, &p.x, &p.y);
            g_print ("\n===== UP->x:%f, y:%f =====\n", p.x, p.y);
            break;

        case CLUTTER_KEY_Down:
            item = example_menu_select_next ((ExampleMenu*)menu);
            clutter_actor_get_position (item, &p.x, &p.y);
            g_print ("\n===== DOWN->x:%f, y:%f =====\n", p.x, p.y);
            break;

        case CLUTTER_KEY_Return:
        case CLUTTER_KEY_KP_Enter:
            example_menu_activate_item ((ExampleMenu*)menu);
            break;
    }

    if (item != NULL)
        clutter_scroll_actor_scroll_to_point (CLUTTER_SCROLL_ACTOR (scroll),
                                              &p);

    return CLUTTER_EVENT_PROPAGATE;
}

static void
on_model_item_selection (GObject* model_item, GParamSpec* pspec, gpointer data)
{
    char* label = NULL;
    gboolean is_selected = FALSE;

    g_object_get (model_item, "label", &label, "selected", &is_selected, NULL);

    if (is_selected)
        g_print ("Item '%s' selected!\n", label);

    g_free (label);
}

static ClutterActor*
create_menu_actor (void)
{
    /* Our store of menu item models */
    GListStore* model = g_list_store_new (EXAMPLE_TYPE_MENU_ITEM_MODEL);
    ClutterActor* menu = g_object_new (EXAMPLE_TYPE_MENU, NULL);
    int i;

    /* Populate the model */
    for (i = 0; i < 12; i++)
    {
        char* label = g_strdup_printf ("Option %02d", i + 1);

        ExampleMenuItemModel* item =
            g_object_new (EXAMPLE_TYPE_MENU_ITEM_MODEL, "label", label, NULL);

        g_list_store_append (model, item);

        g_signal_connect (item,
                          "notify::selected",
                          G_CALLBACK (on_model_item_selection),
                          NULL);

        g_object_unref (item);
        g_free (label);
    }

    /* Bind the list of menu item models to the menu actor; this will
     * create ClutterActor views of each item in the model, and add them
     * to the menu actor
     */
    clutter_actor_bind_model_with_properties (
        menu,
        G_LIST_MODEL (model),
        EXAMPLE_TYPE_MENU_ITEM_VIEW,
        "label",
        "text",
        G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE,
        "selected",
        "selected",
        G_BINDING_BIDIRECTIONAL | G_BINDING_SYNC_CREATE,
        NULL);

    /* We don't need a pointer to the model any more, so we transfer ownership
     * to the menu actor; this means that the model will go away when the menu
     * actor is destroyed
     */
    g_object_unref (model);

    /* Select the first item in the menu */
    example_menu_select_item ((ExampleMenu*)menu, 0);

    return menu;
}

/* The scrolling container for the menu */
static ClutterActor*
create_scroll_actor (void)
{
    ClutterActor* menu = clutter_scroll_actor_new ();
    clutter_actor_set_name (menu, "scroll");
    clutter_scroll_actor_set_scroll_mode (CLUTTER_SCROLL_ACTOR (menu),
                                          CLUTTER_SCROLL_VERTICALLY);
    clutter_actor_set_easing_duration (menu, 250);
    clutter_actor_add_child (menu, create_menu_actor ());

    return menu;
}

int
main (int argc, char* argv[])
{
    ClutterActor *stage, *menu;

    if (clutter_init (&argc, &argv) != CLUTTER_INIT_SUCCESS)
        return 1;

    stage = clutter_stage_new ();
    clutter_stage_set_title (CLUTTER_STAGE (stage), "Actor Model");
    clutter_stage_set_user_resizable (CLUTTER_STAGE (stage), TRUE);
    g_signal_connect (stage, "destroy", G_CALLBACK (clutter_main_quit), NULL);
    g_signal_connect (stage, "key-press-event", G_CALLBACK (on_key_press), NULL);
    clutter_actor_show (stage);

#define PADDING 18.f

    menu = create_scroll_actor ();
    clutter_actor_set_position (menu, 0, PADDING);
    ClutterConstraint* new_constraint =
        clutter_align_constraint_new (stage, CLUTTER_ALIGN_X_AXIS, 0.5);
    clutter_actor_add_constraint (menu, new_constraint);
    ClutterConstraint* bind_constraint =
        clutter_bind_constraint_new (stage, CLUTTER_BIND_HEIGHT, -PADDING * 2);
    clutter_actor_add_constraint (menu, bind_constraint);
    clutter_actor_add_child (stage, menu);

    clutter_main ();

    return 0;
}
/* }}} */